// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import { z } from 'zod';

export const ONE_TIME_DONATION_CONFIG_ID = '1';
export const BOOST_ID = 'BOOST';

export const donationStateSchema = z.enum([
  'INTENT',
  'INTENT_METHOD',
  'INTENT_CONFIRMED',
  'INTENT_REDIRECT',
  'RECEIPT',
  'DONE',
]);

export type DonationStateType = z.infer<typeof donationStateSchema>;

export const donationErrorTypeSchema = z.enum([
  // Used if the user is redirected back from validation, but continuing forward fails
  'Failed3dsValidation',
  // Any other error
  'GeneralError',
  // Any 4xx error when adding payment method or confirming intent
  'PaymentDeclined',
  // When it's been too long since the last step of the donation, and card wasn't charged
  'TimedOut',
  // When donation succeeds but badge application fails
  'BadgeApplicationFailed',
]);
export type DonationErrorType = z.infer<typeof donationErrorTypeSchema>;

const coreDataSchema = z.object({
  // Guid used to prevent duplicates at stripe and in our db
  id: z.string(),

  // Currency code, like USD
  currencyType: z.string(),

  // Cents as whole numbers, so multiply by 100
  paymentAmount: z.number(),

  // The last time we transitioned into a new state.
  timestamp: z.number(),
});
export type CoreData = z.infer<typeof coreDataSchema>;

// Payment type: CARD
export type CardDetail = {
  // Two digits
  expirationMonth: string;

  // Four digts
  expirationYear: string;

  // String with no separators, just 16 digits
  number: string;

  // String
  cvc: string;
};

const stripeDataSchema = z.object({
  // Received after creation of intent
  clientSecret: z.string(),

  // Parsed out of clientSecret - it's everything up to the '_secret_'
  // https://docs.stripe.com/api/payment_intents/object
  paymentIntentId: z.string(),

  // Used for any validation that takes the user somewhere else
  returnToken: z.string(),
});
export type StripeData = z.infer<typeof stripeDataSchema>;

// We need these for durability. if we keep these around throughout the process, retries
// later in the process won't give us weird errors.
// Generated by libsignal.
const receiptContextSchema = z.object({
  receiptCredentialRequestContextBase64: z.string(),
  receiptCredentialRequestBase64: z.string(),
});
export type ReceiptContext = z.infer<typeof receiptContextSchema>;

export const donationReceiptSchema = z.object({
  ...coreDataSchema.shape,
});
export type DonationReceipt = z.infer<typeof donationReceiptSchema>;

export const donationWorkflowSchema = z.discriminatedUnion('type', [
  z.object({
    // Track that user has chosen currency and amount, and we've successfully fetched an
    // intent. There is no need to persist this, because we'd need to update
    // currency/amount on the intent if we want to continue to use it.
    type: z.literal(donationStateSchema.Enum.INTENT),
    ...coreDataSchema.shape,
    ...stripeDataSchema.shape,
  }),

  z.object({
    // Once we are here, we can proceed without further user input. The user has entered
    // payment details and pressed the button to make the payment, and we have sent that
    // to stripe, which has saved that data behind a paymentMethodId. The only thing
    // that might require further user interaction: 3ds validation - see INTENT_REDIRECT.
    type: z.literal(donationStateSchema.Enum.INTENT_METHOD),

    // Stripe persists the user's payment information for us, behind this id
    paymentMethodId: z.string(),

    ...coreDataSchema.shape,
    ...stripeDataSchema.shape,
  }),

  z.object({
    // By this point, Stripe is attempting to charge the user's provided payment method.
    // However it will take some time (usually seconds, sometimes minutes or 1 day) to
    // finalize the transaction. We will only know when we successfully get a receipt
    // credential from the chat server.
    type: z.literal(donationStateSchema.Enum.INTENT_CONFIRMED),

    ...coreDataSchema.shape,
    ...stripeDataSchema.shape,
    ...receiptContextSchema.shape,
  }),

  z.object({
    // An alternate state to INTENT_CONFIRMED. A response from Stripe indicated
    // the user's card requires 3ds authentication, so we need to redirect to their
    // bank, which will complete verification, then redirect back to us. We hand that
    // service a token to connect it back to this process. If the user never comes back,
    // we need to offer the redirect again.
    type: z.literal(donationStateSchema.Enum.INTENT_REDIRECT),

    // Where user should be sent; in this state we are waiting for them to come back
    redirectTarget: z.string(),

    ...coreDataSchema.shape,
    ...stripeDataSchema.shape,
    ...receiptContextSchema.shape,
  }),

  z.object({
    // We now have everything we need to redeem. We know the payment has gone through
    // successfully; we just need to redeem it on the server anonymously.
    type: z.literal(donationStateSchema.Enum.RECEIPT),

    // The result of mixing the receiptCredentialResponse from the API from our
    // previously-generated receiptCredentialRequestContext
    receiptCredentialBase64: z.string(),

    ...coreDataSchema.shape,
  }),

  z.object({
    // After everything is done, we should notify the user the donation succeeded.
    // After we show a notification, or if the user initiates a new donation,
    // then this workflow can be deleted.
    type: z.literal(donationStateSchema.Enum.DONE),
    id: coreDataSchema.shape.id,
    timestamp: coreDataSchema.shape.timestamp,
  }),
]);

export type DonationWorkflow = z.infer<typeof donationWorkflowSchema>;

export const humanDonationAmountSchema = z
  .number()
  .nonnegative()
  .brand('humanAmount');

export type HumanDonationAmount = z.infer<typeof humanDonationAmountSchema>;

// Always in currency minor units e.g. 1000 for 10 USD, 10 for 10 JPY
// https://docs.stripe.com/currencies#minor-units
export const stripeDonationAmountSchema = z
  .number()
  .nonnegative()
  .brand('stripeAmount');

export type StripeDonationAmount = z.infer<typeof stripeDonationAmountSchema>;

export const subscriptionConfigurationCurrencyZod = z.object({
  minimum: humanDonationAmountSchema,
  oneTime: z.record(z.string(), humanDonationAmountSchema.array()),
});

export const oneTimeDonationAmountsZod = z.record(
  z.string(),
  subscriptionConfigurationCurrencyZod
);

export type OneTimeDonationHumanAmounts = z.infer<
  typeof oneTimeDonationAmountsZod
>;
